package com.xunmall.security.acl;

import com.google.common.collect.Maps;
import com.xunmall.base.dto.Pair;
import com.xunmall.base.util.SpringUtils;
import com.xunmall.base.util.StringUtils;
import com.xunmall.base.util.reflection.ReflectionUtils;
import com.xunmall.security.SpringSecurityUtils;
import com.xunmall.security.acl.annotation.Acl;
import com.xunmall.security.acl.annotation.ControlMode;
import com.xunmall.security.acl.annotation.ValueType;
import com.xunmall.security.acl.entity.Metadata;
import com.xunmall.security.acl.service.MetadataService;
import com.xunmall.security.acl.utils.SqlModifierHelper;
import com.xunmall.security.acl.utils.SqlWrapHelper;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.DefaultReflectorFactory;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.ReflectorFactory;
import org.apache.ibatis.reflection.factory.DefaultObjectFactory;
import org.apache.ibatis.reflection.factory.ObjectFactory;
import org.apache.ibatis.reflection.wrapper.DefaultObjectWrapperFactory;
import org.apache.ibatis.reflection.wrapper.ObjectWrapperFactory;
import org.springframework.security.core.Authentication;

import java.lang.reflect.Method;
import java.sql.Connection;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

/**
 * @Author: WangYanjing
 * @Date: 2018/12/27 10:53
 * @Description: 数据权限控制的mybatis拦截器
 */
@Slf4j
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class AclInterceptor implements Interceptor {

    private static final ObjectFactory DEFAULT_OBJECT_FACTORY = new DefaultObjectFactory();
    private static final ObjectWrapperFactory DEFAULT_OBJECT_WRAPPER_FACTORY = new DefaultObjectWrapperFactory();
    private static final ReflectorFactory DEFAULT_REFLECTOR_FACTORY = new DefaultReflectorFactory();

    private static Map<String, Pair<String, ValueType>> acl = Maps.newConcurrentMap();  //表名,字段名,valueType

    public static Map<String, Pair<String, ValueType>> getAcl() {
        return acl;
    }

    public static synchronized void loadAclMetadata() {
        try {
            MetadataService service = SpringUtils.getBean(MetadataService.class);
            List<Metadata> list = service.getAll();

            for (Metadata metadata : list) {
                String tableName = metadata.getTableName().toUpperCase();
                String columnName = metadata.getColumnName().toUpperCase();
                ValueType valueType = ValueType.valueOf(metadata.getValueType());
                Pair pair = new Pair(columnName, valueType);
                acl.put(tableName, pair);
            }
            log.info("loadAclMetadata finish.");
        } catch (Exception e) {
            log.error("loadAclMetadata error:" + e.getMessage(), e);
        }
    }

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        long startTime = System.currentTimeMillis();

        Authentication authentication = SpringSecurityUtils.getAuthentication();
        boolean isLogin = authentication != null && authentication.isAuthenticated() && !"anonymousUser".equals(authentication.getPrincipal());
        if (isLogin) {
//			boolean hasDeparments = BanbuSessionUtil.getDepartments() != null && BanbuSessionUtil.getDepartments().size() > 0;
//			boolean notGlobal = BanbuSessionUtil.getDepartments() != null && !BanbuSessionUtil.getDepartments().contains(SecurityConstants.COMPUUID_GLOBAL);
            //if (notGlobal && hasDeparments) {  //已登录，但不是全公司可见
            if (true) {  //已登录，但不是全公司可见
                //获取mybatis相关参数
                StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
                BoundSql boundSql = statementHandler.getBoundSql();
                String originalSql = boundSql.getSql();
                MetaObject metaStatementHandler = MetaObject.forObject(statementHandler, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, DEFAULT_REFLECTOR_FACTORY);
                MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement");
                SqlCommandType type = mappedStatement.getSqlCommandType();

                //获取ACL注解
                String mapperName = mappedStatement.getId().substring(0, mappedStatement.getId().lastIndexOf("."));
                String methodName = mappedStatement.getId().substring(mappedStatement.getId().lastIndexOf(".") + 1);
                Object mapperProxy = SpringUtils.getBean(Class.forName(mapperName));//org.apache.ibatis.binding.MapperProxy
                Class mapper = (Class) ReflectionUtils.getFieldValue(ReflectionUtils.getFieldValue(mapperProxy, "h"), "mapperInterface");
                Set<Method> mapperMethods = ((Map) ReflectionUtils.getFieldValue(ReflectionUtils.getFieldValue(mapperProxy, "h"), "methodCache")).keySet();

                Acl requestAcl = null;
                if (mapper.isAnnotationPresent(Acl.class)) {
                    requestAcl = (Acl) mapper.getAnnotation(Acl.class);
                } else if (mapperMethods != null && mapperMethods.size() > 0) {
                    for (Method method : mapperMethods) {
                        if ((methodName.equalsIgnoreCase(method.getName()) || methodName.equalsIgnoreCase(method.getName() + "_COUNT")) && method.isAnnotationPresent(Acl.class)) {
                            requestAcl = method.getAnnotation(Acl.class);
                            break;
                        }
                    }
                }

                if (requestAcl != null) { //有ACL注解
                    ControlMode controlMode = requestAcl.controlMode();
                    String newSql = "";
                    boolean change = false;
                    switch (controlMode) {
                        case NONE:
                            break;
                        case SQL_WRAP:
                            String columnName = requestAcl.columnName();
                            ValueType vt = requestAcl.valueType();
                            newSql = processSqlWrap(type, originalSql, columnName, vt);
                            change = true;
                            break;
                        case SQL_MODIFY:
                            Pair<Boolean, String> result = processSqlModify(type, originalSql);
                            if (result != null) {
                                change = result.getFst();
                                newSql = result.getSnd();
                            }
                    }

                    //sql生效
                    if (change && StringUtils.isNotEmpty(newSql)) { //sql被解析且处理结果正常
                        log.debug("originalSql:" + originalSql);
                        log.debug("newSql     :" + newSql);
                        Long endTime = System.currentTimeMillis();
                        log.debug("time(ms)   :" + (endTime - startTime));
                        metaStatementHandler.setValue("delegate.boundSql.sql", newSql);
                    }
                }
            }
        }

        return invocation.proceed();
    }

    private Pair<Boolean, String> processSqlModify(SqlCommandType type, String originalSql) {
        Pair<Boolean, String> result = null;
        switch (type) {
            case SELECT:
                result = SqlModifierHelper.processSql(originalSql);
                break;
            case UPDATE:
            case DELETE:
            case INSERT:
                break;
        }
        return result;
    }

    private String processSqlWrap(SqlCommandType type, String originalSql, String columnName, ValueType vt) {
        String newSql = "";
        switch (type) {
            case SELECT:
                newSql = SqlWrapHelper.processSql(originalSql, columnName, vt);
                break;
            case UPDATE:
            case DELETE:
            case INSERT:
                break;
        }
        return newSql;
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
    }
}
